// This program runs an F.P.S. (First-Person Shooter) game that will also have its own toolset to create maps and games in.
// It will hopefully end up incorporating conversations, quests and an inventory.
//
// Version 1.0 Release
//	by Edward Willoughby
//
// Version 1.1 Release
//	by Edward Willoughby
// - Added versions so that previous version's modules will still load
// - Added end positions so that levels can be completed
// - Added functionality so the sidebars can now be widened or thinned by clicking and dragging the borders
// - Permitted multiple end positions to be saved in the module file
// - Fixed a bug that truncated the lookat angle when exporting a module file causing the player to not look in the correct direction
// - Fixed a bug which caused the player to look at the same vertical angle (theta) when starting a new level/game
//
// Version 1.2 Release
//  by Edward Willoughby
// - Added a 'master' vector of the models so they're only loaded in once
//-----------------

// DirectInput version - must go before loading the dinput.h library
#define DIRECTINPUT_VERSION 0x0800

//-----------------
// Include libraries and other files
#include <cassert>		// for asserts
#include <d3dx9.h>		// Direct 3D library (for all DirectX functions)
#include <dirent.h>		// for searching through files in a directory
#include <vector>
#include <Windows.h>	// Windows library (for window functions, menus, dialog boxes etc.)

#include "Audio.h"
#include "constants.h"
#include "Cursor.h"
#include "FileManager.h"
#include "Level.h"
#include "MainMenu.h"
#include "Player.h"
#include "Toolset.h"
#include "UserInput.h"
#include "UserInterface.h"
#include "Window.h"
//-----------------

// Things to do :-
//#################
// Add scrollbars to the sidebars
// Create pop-up windows for properties etc. (i.e. the DialogueWindow class)
// Allow the user to type a module name when saving and then to select the module they want to edit/play
// Implement a collision engine with objects in-game
// Implement the in-game H.U.D.
// Complete the classes Macrobar
//#################

// Things to ask :-
//#################
// ---How do I render an object with opacity? (i.e. Mesh::render_opacity())
// Would it be a good idea to force the designer to use a naming convention with the waypoints to create a trigger box?
// ---Is there a way I can have a buttons array in the base class (Toolbar)? <-- pointer to an array and then use new?
//#################

// -- Global Variables --
// DirectX globals
LPDIRECT3D9			gp_D3D			= NULL;		// the direct3D object
LPDIRECT3DDEVICE9	gp_d3dDevice	= NULL;		// the rendering device

// global for the current state
int g_state( STATE_MENU);

// globals for display and control/input
Window g_window;
UserInterface* gp_ui( nullptr);
UserInput g_userInput;
Cursor g_mouse;

// global pointer to the toolset
Toolset* gp_toolset( nullptr);

// globals for the module
Level* gp_level( nullptr);

// global for holding all the 3D models in one place (and preventing duplicates)
std::vector<Mesh> g_models;

// global for the main menu screen
MainMenu g_menu_main;

// global for the player
Player g_player( 0, 0, 0);

// structure of a vertex in a vertex buffer
#define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZ | D3DFVF_DIFFUSE)

//-----------------
//// FUNCTION - setup_d3d
// initialise direct 3D
HRESULT setup_d3d( HWND hWnd) {
	// create the D3D object; return failure if this can't be done
	if( NULL == (gp_D3D = Direct3DCreate9( D3D_SDK_VERSION)))
		return E_FAIL;

	// setup the structure used to create the D3DDevice
	D3DPRESENT_PARAMETERS d3dpp;
	ZeroMemory( &d3dpp, sizeof( d3dpp));
	d3dpp.Windowed = TRUE;
	d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
	d3dpp.BackBufferFormat = D3DFMT_X8R8G8B8;
	d3dpp.EnableAutoDepthStencil = TRUE;
	d3dpp.AutoDepthStencilFormat = D3DFMT_D16;

	// create the d3d device
	if( FAILED( gp_D3D->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd, D3DCREATE_SOFTWARE_VERTEXPROCESSING, &d3dpp, &gp_d3dDevice)))
		return E_FAIL;

	// enable the lighting
	gp_d3dDevice->SetRenderState( D3DRS_LIGHTING, TRUE);

	// turn on d3d depth buffer to ensure polygons are rendered correctly according to their depth
	gp_d3dDevice->SetRenderState( D3DRS_ZENABLE, D3DZB_TRUE);

	// turn on d3d backface culling to ensure both faces of triangles are rendered (clockwise & anti-clockwise)
	gp_d3dDevice->SetRenderState( D3DRS_CULLMODE, D3DCULL_NONE);

	return S_OK;
}

//// FUNCTION - clean_up
// release (delete) all the resources used by this program
VOID clean_up()
{
	if( gp_d3dDevice != NULL) {
		gp_d3dDevice->Release();
		gp_d3dDevice = NULL;
	}

	if( gp_D3D != NULL) {
		gp_D3D->Release();
		gp_D3D = NULL;
	}
}

//// FUNCTION - setup_geometry
// setup the program geometry
HRESULT setup_geometry()
{
	HRESULT hr;

	hr =		g_menu_main.setup_geometry(	gp_d3dDevice);
	hr = hr |	g_mouse.setup_geometry(		gp_d3dDevice);

	return hr;
}

//// FUNCTION - setup_lights
// setup the lights for the scene - this will likely be put in the Map class
void setup_lights()
{
	D3DLIGHT9 main_light;
	ZeroMemory(&main_light, sizeof(D3DLIGHT9));
	main_light.Type = D3DLIGHT_POINT;

	main_light.Diffuse.r = 1.0f;
	main_light.Diffuse.g = 1.0f;
	main_light.Diffuse.b = 1.0f;

	main_light.Position.x =  80.0f;
	main_light.Position.y = 100.0f;
	main_light.Position.z =  65.0f;

	main_light.Attenuation0 = 1.0f;
	main_light.Attenuation1 = 0.0f;
	main_light.Attenuation2 = 0.0f;

	main_light.Range = 500.0f;

	// Select and enable the light.
	gp_d3dDevice->SetLight(1, &main_light);
	gp_d3dDevice->LightEnable(1, TRUE);
}

//// FUNCTION - setup_perspective
// setup the view matricies for the perspective objects
void setup_perspective()
{
	D3DXMATRIX mat_view;

	if( g_state == STATE_TOOLSET) {
		D3DXVECTOR3 pos(		  g_player.get_x(), g_player.get_y() + g_player.get_height(), g_player.get_z());
		D3DXVECTOR3 vLookat(	((gp_toolset->get_grid_size_x()/2)*GRID_SIZE), 0.0, ((gp_toolset->get_grid_size_z()/2)*GRID_SIZE));
		D3DXVECTOR3 vUpVector(	  0.0f, 1.0f, 0.0f);
		D3DXMatrixLookAtLH( &mat_view, &pos, &vLookat, &vUpVector);
	}
	else {
		D3DXVECTOR3 pos(		g_player.get_x(), g_player.get_y() + g_player.get_height(), g_player.get_z());
		D3DXVECTOR3 vLookat(	g_player.get_look_at_x(), g_player.get_look_at_y(), g_player.get_look_at_z());
		D3DXVECTOR3 vUpVector(	0.0f, 1.0f, 0.0f);
		D3DXMatrixLookAtLH( &mat_view, &pos, &vLookat, &vUpVector);
	}
	gp_d3dDevice->SetTransform( D3DTS_VIEW, &mat_view);

	// setup the projection matrix
	// this transforms the 2D geometry into 3D space
	D3DXMATRIX mat_proj;
	D3DXMatrixPerspectiveFovLH( &mat_proj, D3DX_PI/4, 1.0f, 1.0f, 700.0f);
	gp_d3dDevice->SetTransform( D3DTS_PROJECTION, &mat_proj);
	gp_d3dDevice->SetRenderState( D3DRS_LIGHTING, TRUE);
	gp_d3dDevice->SetRenderState( D3DRS_ZENABLE, TRUE);
}

//// FUNCTION - setup_orthographic
// setut the view matricies for the orthographic objects
void setup_orthographic()
{
	D3DXMATRIX ortho_proj;

	D3DXMatrixIdentity( &ortho_proj);
	gp_d3dDevice->SetTransform( D3DTS_WORLD, &ortho_proj);
	gp_d3dDevice->SetTransform( D3DTS_VIEW, &ortho_proj);

	D3DXMatrixOrthoLH( &ortho_proj, 1.0f, 1.0f, 1.0f, 2.0f);
	gp_d3dDevice->SetTransform( D3DTS_PROJECTION, &ortho_proj);
	gp_d3dDevice->SetRenderState( D3DRS_LIGHTING, FALSE);
	gp_d3dDevice->SetRenderState( D3DRS_ZENABLE, FALSE);
	//gp_d3dDevice->SetRenderState( D3DRS_CULLMODE, D3DCULL_NONE);
	//gp_d3dDevice->SetRenderState( D3DRS_FILLMODE, D3DFILL_WIREFRAME);
}

//-----------------
//########################################     RENDER - START     ########################################
//########################################################################################################
// The following render functions could go in their own render class and possibly have their own functions that depend on the game's state

// These are purely placeholder functions for now and don't have anything in them
void render_perspective_test() {}

void render_orthographic_test() {}


//// FUNCTION - render_perspective_play
// output the perspective objects for the current level
void render_perspective_play()
{
	// Pre-conditions :- none
	assert( true);

	gp_level->render_perspective();
}

//// FUNCTION - render_perspective
// calls the appropriate function to render the orthographic objects in the scene
void render_perspective()
{
	// Pre-conditions :- none
	assert( true);

	switch( g_state) {
		case STATE_MENU:
			// there shouldn't be any perspective objects on the main menu
			break;
		case STATE_PLAY:
			render_perspective_play();
			break;
		case STATE_TOOLSET:
			gp_toolset->render_perspective();
			break;
		case STATE_TEST:
			render_perspective_test();
	}
}

//// FUNCTION - render_orthographic_menu
// output the orthographic menu objects to the device
void render_orthographic_menu() {
	// Pre-conditions :- the device must exist
	assert( gp_d3dDevice != nullptr);

	// activate the cursor (as it should always be on the main menu)
	g_mouse.set_visible( true);

	// render the background and menu options
	g_menu_main.bg.render( g_menu_main.bg_texture, 0, 0, 1.0, 0);
	g_menu_main.render();
	gp_ui->render_text( "Version "+versions[versions.size()-2], 50, g_window.get_screen_height()-CHAR_HEIGHT-10, 255, 255, 255, 255);
}

//// FUNCTION - render_orthographic_play
// output the orthographic objects during play
void render_orthographic_play()
{
	// Pre-conditions :- none
	assert( true);

	// the mouse should never be visible during gameplay
	g_mouse.set_visible( false);
}

//// FUNCTION - render_orthographic
// calls the appropriate function to render the orthographic objects in the scene
void render_orthographic()
{
	switch( g_state) {
		case STATE_MENU:
			render_orthographic_menu();
			break;
		case STATE_PLAY:
			render_orthographic_play();
			break;
		case STATE_TOOLSET:
			gp_toolset->render_orthographic( g_window, g_mouse);
			break;
		case STATE_TEST:
			render_orthographic_test();
	}

	// render the cursor
	g_mouse.render( 1.3f, 1.0f, float( g_window.get_screen_width()), float( g_window.get_screen_height()));
}

//// FUNCTION - render_hud
// renders the heads-up-display - will probably need a different function for each state as well
void render_hud()
{

}
//##########################################################################################################
//########################################     RENDER - END     ############################################
//-----------------

//-----------------
//########################################     INPUT - START     ########################################
//#######################################################################################################
// The following functions deal with input from the mouse and keyboard

//// FUNCTION - process_input_menu
// process the input for the main menu screen
void process_input_menu()
{
	// Check to see if it should be an active icon (i.e. if it's hovering over a button).
	// The 'y' test requires the window height as DirectInput emanates from the bottom left,
	// while the text boxes reference the top left of the window.
	if( g_menu_main.mouseover( g_mouse, g_window.get_screen_height())) {
		if( g_userInput.button_is_pressed( DIMOUSE_LEFTBUTTON)) {
			// react if a button was pressed, otherwise it'll do nothing
			switch( g_menu_main.get_button( g_mouse, g_window.get_screen_height())) {
				case 0:  // start a new game
					g_player.set_height( 12);
					//g_menu_main.view_modules( true);
					gp_level->set_module( FILE_MODULE_TEST);
					g_state = STATE_PLAY;
					break;
				//case 1:  // view the instructions
				//	g_menu_main.view_instructions( true);
				//	break;
				//case 2:  // display the options screen
				//	g_menu_main.view_options( true);
				//	break;
				case 3:  // start up the toolset
					gp_toolset->set_grid_size( 20, 20);
					g_player.set_height( 0);
					g_player.set_phi( 3.3);
					g_player.set_theta( 1);
					g_state = STATE_TOOLSET;
					break;
				case 4:  // quit the game
					PostQuitMessage( 0);
			}
		}
	}

	// quit the game if escape is pressed
	if (g_userInput.key_is_pressed( DIK_ESCAPE))
		PostQuitMessage( 0);
}

//// FUNCTION - process_input_play
// process the input for game play
void process_input_play()
{
	int look_sensitivity( 100);
	float mouseX( 0), mouseY( 0);
	g_userInput.get_mouse_movement( mouseX, mouseY);
	//g_userInput.get_mouse_position( g_cursor_x, g_cursor_y, g_window.get_screen_width(), g_window.get_screen_height());
	
	if( mouseX != 0) {
		// rotate the camera by modifying the lookat based on the movement of the mouse
		if( mouseX < 0)
			g_player.set_phi( g_player.get_phi() - mouseX/look_sensitivity);
		else
			g_player.set_phi( g_player.get_phi() - mouseX/look_sensitivity);
	}

	if (mouseY != 0) {
		// move the camera up/down based on the movement of the mouse
		if( mouseY > 0)
			g_player.set_theta( g_player.get_theta() + mouseY/100 * g_player.get_mouse_invert());
		else if( mouseY < 0)
			g_player.set_theta( g_player.get_theta() + mouseY/100 * g_player.get_mouse_invert());

		// prevents the camera from looking too far up or down
		if( g_player.get_theta() > D3DX_PI/2-0.01f * g_player.get_mouse_invert())
			g_player.set_theta( D3DX_PI/2-0.01f * g_player.get_mouse_invert());
		else if( g_player.get_theta() < -D3DX_PI/2+0.01f * g_player.get_mouse_invert())
			g_player.set_theta( -D3DX_PI/2+0.01f * g_player.get_mouse_invert());
	}

	// return to the main menu if escape is pressed
	if( g_userInput.key_is_pressed( DIK_ESCAPE)) {
		gp_level->reset();
		g_state = STATE_MENU;
	}

	if( g_userInput.key_is_held( DIK_W))	// W - move the player (camera) forward)
		g_player.move( 'w');
	if( g_userInput.key_is_held( DIK_S))	// S - move the player (camera) backwards
		g_player.move( 's');
	if( g_userInput.key_is_held( DIK_A))	// A - move the player (camera) left
		g_player.move( 'a');
	if( g_userInput.key_is_held( DIK_D))	// D - move the player (camera) right
		g_player.move( 'd');
	
	// ##### NOTE #####
	// The bobbing during movement is not yet perfected and it isn't that high on the priority list right now... so sue me(!)

	//if( !g_userInput.key_is_held(DIK_D) && !g_userInput.key_is_held(DIK_W) && !g_userInput.key_is_held(DIK_A) && !g_userInput.key_is_held(DIK_S)
	//	|| g_userInput.key_is_held(DIK_D) && g_userInput.key_is_held(DIK_A) || g_userInput.key_is_held(DIK_W) && g_userInput.key_is_held(DIK_S)) {
	//		g_player.update_bob( false);	
	//		g_player.set_running( false);
	//}
	//else
	//	g_player.update_bob(true);
	// ##### NOTE #####

	if( g_userInput.key_is_held( DIK_LCONTROL)) {		// LEFT CRTL - make the player (camera) crouch
		g_player.set_height( PLAYER_HEIGHT_CROUCH);
		g_player.set_walking( true);
	}
	else if( g_userInput.key_is_held( DIK_LSHIFT)) {	// LEFT SHIFT - make the player (camera) walk
		g_player.set_height( PLAYER_HEIGHT_VERTICAL);
		g_player.set_walking( true);
	}
	else {												// if the player is neither crouching nor walking then move at normal speed and height
		g_player.set_height( PLAYER_HEIGHT_VERTICAL);
		g_player.set_walking( false);
	}
}

//// FUNCTION - process_input_test
// process the input for testing the module in the toolset
void process_input_test() {}

//// FUNCTION - process_input
// call the correct input function for the current game state
void process_input()
{
	// update the mouse's position
	// when setting the mouse's position it needs to be divided by the window size to fit on the 1x1 orthographic display
	float mouse_x( g_mouse.get_x()), mouse_y( g_mouse.get_y());
	g_userInput.get_mouse_position( mouse_x, mouse_y, g_window.get_screen_width(), g_window.get_screen_height());
	g_mouse.set_position( mouse_x, mouse_y);

	switch( g_state) {
		case STATE_MENU:
			process_input_menu();
			break;
		case STATE_PLAY:
			process_input_play();
			break;
		case STATE_TOOLSET:
			gp_toolset->process_input_toolset( g_userInput, g_player, g_state, STATE_MENU, g_mouse.get_x(), g_window.invert_y( g_mouse.get_y()));
			break;
		case STATE_TEST:
			process_input_test();
	}

	if (g_userInput.key_is_pressed( DIK_Q))
		// 'Q' has been pressed - so tell the application to exit.
		PostQuitMessage( 0);
}
//########################################     INPUT - END     ########################################
//#####################################################################################################
//-----------------

//-----------------
//########################################     INTERACTION - START     ########################################
//#############################################################################################################
//// FUNCTION - process_interaction_play
// process iterataction that occurs during gameplay
void process_interaction_play()
{
	gp_level->collision( &g_player, g_state);
}

//// FUNCTION - process_interaction
// call the correct interaction function depending on the game state
void process_interaction()
{
	switch( g_state) {
		case STATE_MENU:
			//process_interaction_menu();
			break;
		case STATE_PLAY:
			process_interaction_play();
			break;
		case STATE_TOOLSET:
			gp_toolset->process_interaction( g_window, g_mouse);
			break;
		case STATE_TEST:
			//process_interaction_test();
			break;
	}
}

//-----------------
//// FUNCTION - WinMain
// starts up the program and creates the window that will be used
int WINAPI WinMain( HINSTANCE hInst, HINSTANCE, LPSTR, int)
{

	// initialise Direct3D
	if( SUCCEEDED( setup_d3d( g_window.get_hWnd()))) {
		// create the scene geometry
		if( SUCCEEDED( setup_geometry())) {
			// initialise DirectInput and acquire the keyboard
			if( SUCCEEDED( g_userInput.setup_direct_input( hInst, g_window.get_hWnd()))) {
				// show the window
				ShowWindow( g_window.get_hWnd(), SW_SHOWDEFAULT);
				UpdateWindow( g_window.get_hWnd());

				{
					// load in all the known meshes from the assets file and store them in the models vector
					FileManager fm( gp_d3dDevice);

					fm.extract_models_and_audio( &g_models);
				}

				// initialise the UserInterface and allocate the global pointer to it
				UserInterface ui( gp_d3dDevice, &g_window);
				gp_ui = &ui;

				// initialise the toolset variable and direct the global pointer to it
				Toolset toolset( gp_d3dDevice, g_window, gp_ui, &g_models);
				gp_toolset = &toolset;

				if( SUCCEEDED( gp_toolset->load_assets())) {
					// initialise the variable which will hold the data pertaining to the current level
					Level level( gp_d3dDevice, &g_models, &g_player);
					gp_level = &level;

					// create the buttons that will be used on the main menu screen (e.g. 'New Game', 'Instructions', 'Quit' etc.)
					g_menu_main.set_text( gp_ui, g_window.get_screen_width(), g_window.get_screen_height());

					// start off with the mouse at the center of the window
					g_mouse.set_position( g_window.get_screen_width()/2, g_window.get_screen_height()/2);

					MSG msg;
					ZeroMemory( &msg, sizeof( msg));
					while( msg.message != WM_QUIT) {
						if( PeekMessage( &msg, NULL, 0U, 0U, PM_REMOVE)) {
							TranslateMessage( &msg);
							DispatchMessage( &msg);
						}
						else {
							setup_lights();

							// clear the backbuffer to a green colour (usually blue; 50, 50, 250)
							gp_d3dDevice->Clear( 0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB( 50, 250, 50), 1.0f, 0);

							// begin the scene
							if( SUCCEEDED( gp_d3dDevice->BeginScene())) {
								setup_perspective();
								render_perspective();

								setup_orthographic();
								render_orthographic();

								render_hud();

								gp_d3dDevice->EndScene();
							}

							// present the backbuffer to the display
							gp_d3dDevice->Present( NULL, NULL, NULL, NULL);

							g_userInput.process_input();
							process_input();
							process_interaction();
						}
					}
				}
				else
					// error message for the toolset setup geometry
					MessageBox( NULL, "Error Code: 006", "Toolset::load_assets", MB_OK);
			}
		}
	}

	clean_up();

	g_window.unregister();
	return 0;
}

////################################################
//// Code to test locating all files within a folder using the 'dirent' header
//DIR *dir;
//struct dirent *ent;
//std::string all( "");
//int y( 50);
//g_file_retrieval_test.set_information( gp_ui);

//dir = opendir( PATH_MODELS.c_str());
//if (dir != NULL) {

//	// print all the files and directories within directory
//	while ((ent = readdir (dir)) != NULL)

//		if( ent->d_type == DT_REG) {
//			g_file_retrieval_test.create_button( ent->d_name, 300, y, 255, 255, 255, 255);
//			y += 30;
//		}
//		//all += ent->d_name;
//	closedir (dir);
//}
//else {
//	// could not open directory
//	MessageBox( NULL, "Error Code: *shrug*", "file_locating", MB_OK);
//	return EXIT_FAILURE;
//}
////################################################